\section{数据整理}
\subsection{统计文档中的内容}
\begin{questionbox}
统计 words 文件 (/usr/share/dict/words) 中包含至少三个 a 且不以 's 结尾的单词个数。这些单词中，出现频率前三的末尾两个字母是什么？ sed 的 y 命令，或者 tr 程序也许可以帮你解决大小写的问题。共存在多少种词尾两字母组合？还有一个很 有挑战性的问题：哪个组合从未出现过？
\end{questionbox}

我们一步一步地解决这个问题。首先我们解决文档中单词大小写的问题。有如下两种方式解决：
\begin{lstlisting}
# 方法1：用 tr（
tr 'A-Z' 'a-z' < /usr/share/dict/words
# 方法2：用 sed y//
sed 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/' /usr/share/dict/words
\end{lstlisting}
具体效果如图\ref{文档中的字母小写}（这里用的是方法1：tr）

输入下面的命令：
\begin{lstlisting}
tr 'A-Z' 'a-z' < /usr/share/dict/words | grep -E '.*a.*a.*a.*' | grep -v "'s$" | wc -l
\end{lstlisting}
在这个命令中\texttt{grep -E '.*a.*a.*a.*'}是找单词中至少有三个\texttt{a}的单词，\texttt{grep -v "'s\$"}是找单词中不易\texttt{'s}结尾的单词，最后通过\texttt{wc -l}输出结果。

\begin{figure}[H]
	\centering
	\includegraphics[width=0.48\linewidth]{assets/images/屏幕截图 2025-09-08 172945.png} \hfill
	\includegraphics[width=0.48\linewidth]{assets/images/屏幕截图 2025-09-08 173211.png}
	\caption{文档中的字母小写}
	\label{文档中的字母小写}
\end{figure}

接下里我们解决第一个问题：\textbf{包含至少三个 a 且不以 's 结尾的单词个数}

具体结果如图\ref{找出含至少3a不是‘s结尾的单词}
\begin{figure}[H]
	\centering
	\includegraphics[width=\linewidth]{assets/images/屏幕截图 2025-09-08 194616.png}
	\caption{找出含至少3a不是‘s结尾的单词}
	\label{找出含至少3a不是‘s结尾的单词}
\end{figure}

接着我们解决第二个问题：\textbf{这些单词中，出现频率前三的末尾两个字母是什么？}

输入下面的代码：
\begin{lstlisting}
tr 'A-Z' 'a-z' < /usr/share/dict/words | \
grep -E '.*a.*a.*a.*' | \
grep -v "'s$" | \
sed 's/.*\(..\)$/\1/' | \
sort | \
uniq -c | \
sort -nr | \
head -3
\end{lstlisting}

结果如图\ref{在这些单词中找出出现频率前三的单词}
\begin{figure}[H]
	\centering
	\includegraphics[width=\linewidth]{assets/images/屏幕截图 2025-09-08 195444.png}
	\caption{在这些单词中找出出现频率前三的单词}
	\label{在这些单词中找出出现频率前三的单词}
\end{figure}

接着我们解决第三个问题：\textbf{共存在多少种词尾两字母组合？}

我们只需要把上一步的\texttt{head -3}换成\texttt{wc -l}即可
\begin{lstlisting}
tr 'A-Z' 'a-z' < /usr/share/dict/words | \
grep -E '.*a.*a.*a.*' | \
grep -v "'s$" | \
sed 's/.*\(..\)$/\1/' | \
sort | \
uniq | \
wc -l
\end{lstlisting}

结果如图\ref{有多少种结尾}
\begin{figure}[H]
	\centering
	\includegraphics[width=\linewidth]{assets/images/屏幕截图 2025-09-08 195930.png}
	\caption{有多少种结尾}
	\label{有多少种结尾}
\end{figure}

最后我们解决进阶问题：\textbf{哪个组合从未出现过}

为了解决这个问题，我们首先需要知道一共有多少种组合
\begin{lstlisting}
printf '%s\n' {a..z}{a..z} > all_pairs.txt
\end{lstlisting}
这个命令可以生成所有的组合，然后存储到\texttt{all\_pairs.txt}中。

然后我们生成实际的组合并存储到\texttt{actual\_pairs.txt}
\begin{lstlisting}
tr 'A-Z' 'a-z' < /usr/share/dict/words | \
grep -E '.*a.*a.*a.*' | \
grep -v "'s$" | \
sed 's/.*\(..\)$/\1/' | \
sort -u > actual_pairs.txt
\end{lstlisting}

最后我们输出结果：
\begin{lstlisting}
grep -vxFf actual_pairs.txt all_pairs.txt
\end{lstlisting}

结果如图\ref{有哪些组合没有出现}
\begin{figure}[H]
	\centering
	\includegraphics[width=\linewidth]{assets/images/屏幕截图 2025-09-08 200600.png}
	\caption{有哪些组合没有出现}
	\label{有哪些组合没有出现}
\end{figure}

\subsection{如何合理使用\texttt{sed}}

\begin{questionbox}
    进行原地替换听上去很有诱惑力，例如：\texttt{sed s/REGEX/SUBSTITUTION/ input.txt > input.txt}。但是这并不是一个明智的做法，为什么呢？还是说只有 sed 是这样的? 查看 man sed 来完成这个问题
\end{questionbox}

我们首先在命令行中输入\texttt{man sed}来查看\texttt{sed}命令的使用说明，结果如图\ref{sed的使用手册}
\begin{figure}[htb]
	\centering
	\includegraphics[width=\linewidth]{assets/images/屏幕截图 2025-09-08 201411.png}
	\caption{sed的使用手册}
	\label{sed的使用手册}
\end{figure}

我们首先尝试一些\texttt{ sed s/REGEX/SUBSTITUTION/ input.txt > input.txt}命令会发生什么，结果如图\ref{sed命令的作用}
\begin{figure}[htb]
	\centering
	\includegraphics[width=\linewidth]{assets/images/屏幕截图 2025-09-08 201924.png}
	\caption{sed命令的作用}
	\label{sed命令的作用}
\end{figure}

这个时候我们使用\texttt{cat}命令查看\texttt{input.txt}文件中的内容发现该文件为空。

为什么会这样？\\
这是因为\texttt{Shell}先处理重定向 >，然后才运行\texttt{sed}。这不是\texttt{sed}命令的错，而是\texttt{Shell}的 I/O 机制决定的.

我们正确的做法应该是\texttt{sed \-i}。同时通过查询资料，我们发现任何命令如果尝试 读取一个文件并同时重定向输出到同一个文件，都会遇到这个问题。

\subsection{查看开机时间}
\begin{questionbox}
    找出您最近十次开机的开机时间平均数、中位数和最长时间。在 Linux 上需要用到\texttt{journalctl}，而在 macOS 上使用\texttt{log show}。找到每次起到开始和结束时的时间戳。
\end{questionbox}
\begin{lstlisting}
journalctl --list-boots
\end{lstlisting}
这个命令可以列出所有启动记录，包括启动ID、启动时间、关闭时间等信息。然后对于每个启动ID，我们可以使用下面的命令来查看详细的信息
\begin{lstlisting}
journalctl --boot=<boot_id> 
\end{lstlisting}

结合起来我们只需要在bash中输入如下命令就可以查看启动完成的时间。
\begin{lstlisting}
journalctl --boot=<boot_id> | grep 'Startup finished in'
\end{lstlisting}

具体结果如图\ref{查看系统启动时间}
\begin{figure}[htb]
	\centering
	\includegraphics[width=\linewidth]{assets/images/屏幕截图 2025-09-09 094112.png}
	\caption{查看系统启动时间}
	\label{查看系统启动时间}
\end{figure}

我们可以写一个脚本来自动化这个过程进而找到最近十次的启动时间。在脚本中输入如下内容
\begin{lstlisting}
#!/bin/bash

# 获取最近十次启动的ID
boot_ids=$(journalctl --list-boots | awk '{print $1}' | head -n 10)

# 初始化数组存储启动时间
boot_times=()

for boot_id in $boot_ids; do
    if [ "$boot_id" != "-" ]; then
        time=$(journalctl --boot=$boot_id | grep 'Startup finished in' | awk '{print $NF}')
        boot_times+=("$time")
    fi
done

# 输出启动时间
echo "Boot times: ${boot_times[@]}"
\end{lstlisting}

结果如图\ref{最近十次启动的启动完成时间}
\begin{figure}[htb]
	\centering
	\includegraphics[width=\linewidth]{assets/images/屏幕截图 2025-09-09 094905.png}
	\caption{最近十次启动的启动完成时间}
	\label{最近十次启动的启动完成时间}
\end{figure}

同样的我们可以写个脚本来计算平均数、中位数和最长时间
\begin{lstlisting}
# 转换时间并计算
total_time=0
max_time=0
sorted_times=($(printf '%s\n' "${boot_times[@]}" | sort -n))

for time in "${sorted_times[@]}"; do
    total_time=$((total_time + time))
    if [ "$time" -gt "$max_time" ]; then
        max_time=$time
    fi
done

average_time=$((total_time / ${#sorted_times[@]}))
median_time=${sorted_times[$(((${#sorted_times[@]} - 1) / 2))]}

echo "Average boot time: $average_time seconds"
echo "Median boot time: $median_time seconds"
echo "Maximum boot time: $max_time seconds"
\end{lstlisting}

结果如图\ref{计算平均数、中位数和最长时间}
\begin{figure}[htb]
	\centering
	\includegraphics[width=\linewidth]{assets/images/屏幕截图 2025-09-09 095222.png}
	\caption{计算平均数、中位数和最长时间}
	\label{计算平均数、中位数和最长时间}
\end{figure}

\subsection{获取启动日志}
\begin{questionbox}
    查看之前三次重启启动信息中不同的部分(参见 journalctl 的 -b 选项)。将这一任务分为几个步骤，首先获取之前三次启动的启动日志，也许获取启动日志的命令就有合适的选项可以帮助您提取前三次启动的日志，亦或者您可以使用 sed '0,/STRING/d' 来删除 STRING 匹配到的字符串前面的全部内容。然后，过滤掉每次都不相同的部分，例如时间戳。下一步，重复记录输入行并对其计数(可以使用 uniq )。最后，删除所有出现过 3 次的内容（因为这些内容是三次启动日志中的重复部分）。
\end{questionbox}

为了解决这个问题我们可以先了解一下\texttt{journalctl -b N}，这个命令可以查看第N次之前的日志。然后我们可以使用\texttt{sed}命令来查看日志的主体。最后命令如下：
\begin{lstlisting}
{
    journalctl -b -1 | sed 's/.*]: //' | sed 's/.*: //';
    journalctl -b -2 | sed 's/.*]: //' | sed 's/.*: //';
    journalctl -b -3 | sed 's/.*]: //' | sed 's/.*: //';
} | sort | uniq -c | awk '$1 == 1 || $1 == 2' | sort -nr
\end{lstlisting}

结果如图\ref{1}
\begin{figure}[htb]
	\centering
	\includegraphics[width=\linewidth]{assets/images/屏幕截图 2025-09-09 100122.png}
     \caption{输出日志信息}
	\label{1}
\end{figure}

\subsection{在文本中找出匹配项}
\begin{questionbox}
    在一个文本文件 contacts.txt 中，找出所有符合基本格式的电子邮件地址。
\end{questionbox}

为了解决这个问题，我们可以使用 grep 和正则表达式匹配基本邮箱格式
\begin{lstlisting}
grep -Eo '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' emails.txt
\end{lstlisting}
以下是对各个命令的解释:
\begin{itemize}
    \item \texttt{[a-zA-Z0-9.\_\%+-]+}：用户名部分（允许字母、数字、\texttt{.}、\texttt{\_}、\texttt{\%}、\texttt{+}、\texttt{-}）
    \item \texttt{@}：字面量
    \item \texttt{[a-zA-Z0-9.-]+}：域名主机部分
    \item \texttt{\textbackslash.}：点号 \texttt{.}
    \item \texttt{[a-zA-Z]\{2,\}}：顶级域（如 \texttt{com}, \texttt{org}, \texttt{io}, \texttt{uk}）
\end{itemize}

结果如下\ref{文本中找出匹配项}
\begin{figure}[htb]
	\centering
	\includegraphics[width=\linewidth]{assets/images/屏幕截图 2025-09-09 103851.png}
	\caption{文本中找出匹配项}
	\label{文本中找出匹配项}
\end{figure}

\subsection{找出回文单词}
\begin{questionbox}
    能否仅用正则表达式找出回文单词（正读反读一样），如 radar, level, madam？
\end{questionbox}
由于纯正则无法完美解决任意长度回文，所以我们对固定长度做。
输入如下命令：
\begin{lstlisting}
grep -E '^([a-z])([a-z])[a-z]\2\1$' share/words
\end{lstlisting}
对于命令的解释如下：
\begin{itemize}
    \item \texttt{\^{}([a-z])}：第一个字符，捕获为 \texttt{\textbackslash 1}
    \item \texttt{([a-z])}：第二个字符，捕获为 \texttt{\textbackslash 2}
    \item \texttt{[a-z]}：中间字符（第三个）
    \item \texttt{\textbackslash 2\textbackslash 1\$}：倒序匹配第二、第一字符，行尾结束
\end{itemize}

结果如图\ref{回文单词}
\begin{figure}[htb]
	\centering
	\includegraphics[width=\linewidth]{assets/images/屏幕截图 2025-09-09 104536.png}
	\caption{回文单词}
	\label{回文单词}
\end{figure}